package controller;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import messages.JobRequestResult;
import messages.JobSignUpResult;
import model.Job;
import model.Park;
import model.Role;
import model.User;
import model.WorkLoad;
/**
 * This class is to be the GOD class. It will hold all of the Park, Job, and User information while
 * the program is running. It will also be what the GUI will interface with.
 * @author James Gunter - jpgunter
 * @author Ken Norton
 *
 */
public class Controller {
  
  /**
   * A list of all jobs past and present.
   * 
   * Invariant: not null;
   */
  private List<Job> my_jobs;
  /**
   * A list of all users in the system.
   * 
   * Invariant: not null;
   */
  private List<User> my_users;
  /**
   * A list of all parks in the system.
   * 
   * Invariant: not null;
   */
  private List<Park> my_parks;
  /**
   * All of the global options such as mins and maxes.
   * 
   * Invariant: not null;
   */
  private Options my_options;
  /**
   * Class to interact with the file storage.
   * 
   * Invariant: not null;
   */
  private PersistentStorage my_persistant_storage;


  /**
   * Creates the controller and loads the data from persistent Storage.
   * 
   * post: my_jobs has all jobs loaded from persistent storage.
   * post: my_users has all users loaded from persistent storage.
   * post: my_parks has all parks loaded from persistent storage.
   * post: my_options has all options loaded from persistent storage.
   * post: associations between jobs and users are up-to-date.
   * post: associations between jobs and parks are up-to-date.
   */
  public Controller(){
    my_jobs = new ArrayList<Job>();
    my_users = new ArrayList<User>();
    my_parks = new ArrayList<Park>();
    my_options = new Options();


    my_persistant_storage = new PersistentStorage();
    my_persistant_storage.loadData(my_parks, my_users, my_jobs, my_options);
  }

  /**
   * @author James Gunter
   * Sets the list of users to the specified list.
   * 
   * Should be used for testing purposes only.
   * @param the_users A list of Users.
   * 
   * pre: the_users is a non-null List of Objects of type User.
   * post: getUsers() will return the same list of users as the_users.
   */
  public void setUsers(List<User> the_users){
    my_users = the_users;
  }
  /**
   * @author James Gunter
   * Sets the list of jobs to the specified list.
   * 
   * Should be used for testing purposes only.
   * @param the_jobs A list of jobs.
   * 
   * pre: the_jobs is a non-null List of Objects of type Job.
   * post: getJobs() will return the same list of jobs as the_jobs.
   */
  public void setJobs(List<Job> the_jobs) {
    my_jobs = the_jobs;  
  }

  /**
   * @author James Gunter
   * Sets the list of Parks to the specified list.
   * 
   * Should be used for testing purposes only.
   * @param the_parks A list of parks.
   * 
   * pre: the_parks is a non-null List of Objects of type Park.
   * post: getParks() will return the same list of parks as the_parks.
   */
  public void setParks(List<Park> the_parks) {
    my_parks = the_parks;  
  }

  /**
   * @author James Gunter
   * Gets a List of Strings representing all available districts.
   * 
   * @return A List of all districts.
   * 
   * pre: none, can be called at any time.
   * post: nothing has been modified.
   */
  public Collection<String> getDistricts() {
    Set<String> district_set = new HashSet<String>();
    for(Park p: my_parks){
      district_set.add(p.getDistrict());
    }
    return district_set;
  }

  /**
   * @author James Gunter
   * Gets the List of all Regions.
   * 
   * @return The List of all regions.
   * 
   * pre: none, can be called at any time.
   * post: nothing has been modified.
   */
  public Collection<String> getRegions() {
    Set<String> region_set = new HashSet<String>();
    for(Park p: my_parks){
      region_set.add(p.getRegion());
    }
    return region_set;
  }

  /**
   * @author Ken Norton
   * Submits a request for a new {@link Job}. Will return a {@link JobRequestResult} depending on
   * what happens. 
   * @param the_job The {@link Job} you are trying to submit.
   * @param the_user The {@link User} that is trying to submit the request. 
   * @return A {@link JobRequestResult} indicating the result of the request.
   * 
   * pre:  the_job is non-null
   * pre:  the_user is non-null
   * 
   * post: No modifications are made.
   * post: Returns a non-null JobRequestResult.
   */
  public JobRequestResult submitJobRequest(Job the_job, 
      User the_user) {
    JobRequestResult result = JobRequestResult.SUCCESS;

    Date today;
    SimpleDateFormat sdf = 
        new SimpleDateFormat("MM/dd/yyyy");
    try {
      today = sdf.parse(sdf.format(new Date()));
    } catch (final ParseException the_exception) {
      today = new Date();
    }

    // Set max_date to 3 months in the future.
    Calendar cal = Calendar.getInstance();
    cal.set(Calendar.DAY_OF_YEAR, 
        cal.get(Calendar.DAY_OF_YEAR) + 
        my_options.getMaxScheduleAheadDays());
    Date max_date;
    max_date = cal.getTime();

    /* Count Jobs during the current week with the date of 
     * the current job in the middle of the week.
     */
    
    // Set week date to 3 days prior.
    Date week_date;
    cal.setTime(the_job.getDate());
    cal.set(Calendar.DAY_OF_MONTH, cal.get(Calendar.DAY_OF_MONTH) - 3);
    week_date = cal.getTime();
    
    int job_count = 0;
    for (int i = 0; i <= 6; i++) {
      job_count += getJobs(week_date).size();
      cal.set(Calendar.DAY_OF_MONTH, cal.get(Calendar.DAY_OF_MONTH) + 1);
      week_date = cal.getTime();
    }
    
    if (!the_job.getPark().getParkManager().
        equals(the_user)) {
      result = JobRequestResult.WRONG_PARK_MGR;
    } else if (the_job.getDuration() > 2) {
      result = JobRequestResult.EXCEEDS_MAX_DURATION;
    } else if (the_job.getDate().before(today)) {
      result = JobRequestResult.JOB_IN_PAST;
    } else if (the_job.getDate().after(max_date)) {
      result = JobRequestResult.DATE_BEYOND_LIMIT;
    } else if(getPendingJobs().size() >= 
        my_options.getMaxJobs()) {
      result = JobRequestResult.MAX_JOBS_REACHED;
    } else if(job_count >= 5){
      result = JobRequestResult.MAX_WEEKLY_REACHED;
    } else {
      my_jobs.add(the_job);
    }
    return result;
  }
  /**
   * @author James Gunter
   * Gets the {@link Park}s that the specified user is allowed to see.
   * For Volunteers it will get all of the parks (They can sign up for a job at any of them.)
   * For Park Managers it will get all of the parks that they manage.
   * For Staff Members it will get all parks.
   * @param the_user The {@link User} you are requesting a list of {@link Park}s for.
   * @return The list of {@link Park}s.
   * 
   * @pre: the_user is a non-null Object of type User.
   * @pre: the_user is a User in the list returned by getUsers().
   * @post: nothing as changed.
   */
  public List<Park> getParks(User the_user){
    List<Park> found_parks = my_parks;

    //only get the parks the park manager manages.
    if(the_user.getUserRole() == Role.PARK_MANAGER){
      found_parks = new ArrayList<Park>();
      for(Park p: my_parks){
        if(p.getParkManager() == the_user){
          found_parks.add(p);
        }
      }
    }

    return found_parks;
  }
  /**
   * @author Ben Krack
   * Gets all pending {@link Job}s.
   * @return A list of {@link Job}s.
   * 
   * pre: none, can be called at any time.
   * post: nothing has been modified.
   */
  public List<Job> getPendingJobs(){
    List<Job> pending_jobs = new ArrayList<Job>();
    long current_millis = System.currentTimeMillis();
    Calendar calendar1 = Calendar.getInstance();
    calendar1.setTimeInMillis(current_millis);
    Calendar calendar2 = Calendar.getInstance();
    for (Job job: my_jobs) {
      Date date = job.getDate();
      calendar2.setTime(date);
      if (calendar2.after(calendar1)) {
        pending_jobs.add(job);
      }
    }
    return pending_jobs;
  }
  /**
   * @author James Gunter
   * Gets the user that is associated with the specified e-mail address. 
   * @param the_email The e-mail address associated with a user account.
   * @return The {@link User} that is associated with that e-mail or <b>null</b> if not found.
   * 
   * pre: the_email is a non-null object of type String.
   * post: nothing has been modified.
   */
  public User getUserByEmail(String the_email){
    User found = null;
    for(User u: my_users){
      if(u.getUserEmail().equalsIgnoreCase(the_email)){
        found = u;
        break;
      }
    }
    return found;
  }
  /**
   * @author James Gunter
   * Gets the list of all jobs past and pending.
   * @return A list of {@link Job}s
   * 
   * pre: none, can be called at any time.
   * post: nothing has been modified.
   */
  public List<Job> getJobs(){
    return my_jobs;
  }

  /**
   * @author James Gunter
   * Gets a list all jobs occurring on a specific day.
   * @param the_day The date you are searching for.
   * @return A list of jobs that occur on the_day.
   * 
   * pre: the_day is a non-null object of type Date.
   * post: nothing has been modified.
   */
  public List<Job> getJobs(Date the_day){
    List<Job> days_jobs = new ArrayList<Job>();

    for(Job j : my_jobs){
      //I know I am using deprecated methods, but this is easier;
      if( (j.getDate().getYear()) == (the_day.getYear()) &&
          j.getDate().getMonth() == the_day.getMonth() &&
          j.getDate().getDate() == the_day.getDate()){
        days_jobs.add(j);
      }
    }

    return days_jobs;
  }

  /**
   * @author James Gunter
   * Gets the list of jobs that the specified user is allowed to see. 
   * For Volunteers, it gets the list of jobs that the user has signed up for.
   * For Park Managers, it gets the list of all jobs associated with any of the parks they manage.
   * For Staff Members, returns all jobs.
   * @param the_user The user you are requesting a list of jobs for.
   * @return A list of jobs.
   * 
   * pre: the_user is a non-null object of type User.
   * post: nothing has been modified.
   */
  public List<Job> getJobs(User the_user){
    List<Job> found_jobs = my_jobs;
    if(the_user.getUserRole() == Role.VOLUNTEER){
      found_jobs = new ArrayList<Job>();
      for(Job j: my_jobs){
        if(j.hasVolunteer(the_user)){
          found_jobs.add(j);
        }
      }
    } else if (the_user.getUserRole() == Role.PARK_MANAGER){
      found_jobs = new ArrayList<Job>();
      for(Job j: my_jobs){
        if(j.getPark().getParkManager() == the_user){
          found_jobs.add(j);
        }
      }
    }
    return found_jobs;
  }
  /**
   * @author Ken Norton
   * Tries to sign up a user for a job at the specified work load. Will return a JobSignUpResult to
   * indicate what happened with the request. 
   * @param the_user The user trying to sign up for job.
   * @param the_job The job the user is trying to sign up for.
   * @param the_workload The workload they are signing up for.
   * @return A JobSignUpResult to indicate what happened with the request.
   * 
   * pre: the_user is non-null.
   * pre: the_workload is non-null.
   * 
   * post: No modifications.
   * post: Returns a non-null JobSignUpResult.
   */
  public JobSignUpResult signUp(User the_user, Job the_job, WorkLoad the_workload){
    JobSignUpResult result = JobSignUpResult.SUCCESS;
    
    Date today;
    SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
    try {
      today = sdf.parse(sdf.format(new Date()));
    } catch (ParseException e) {
      today = new Date();
    }
    

    /* The checks below are in order of precedence, 
     * so that the most prohibitive reason is returned.
     */
    if (the_user.getUserRole() != Role.VOLUNTEER) {
      result = JobSignUpResult.NOT_A_VOLUNTEER;
    } else if (the_user.getUserRole() == Role.BLACKBALLED) {
      result = JobSignUpResult.BLACKBALLED;
    } else if (today.after(the_job.getDate())) {
      result = JobSignUpResult.JOB_IN_PAST;
    } else if (the_job.getTotalFilled() >= 
        the_job.getTotalNeeded()) {
      result = JobSignUpResult.JOB_FULL;
    } else if (the_job.isFilled(the_workload)) {
      result = JobSignUpResult.WORKLOAD_FULL;
    } else {
      for (Job job : getJobs(the_job.getDate())) {
        for (User user : job.getVolunteers().keySet()) {
          if (the_user.equals(user)) {
            result = JobSignUpResult.OVERCOMMITED;
          }
        }
      }
    }
    
    if (result == JobSignUpResult.SUCCESS) {
      the_job.addVolunteer(the_user, the_workload);
    }

    return result;
  }
  
  /**
   * @author James Gunter
   * Gets a list of all volunteers. 
   * @return A list of volunteers.
   * 
   * pre: none, can be called at any time.
   * post: nothing has been modified.
   */
  public List<User> getVolunteers(){
    List<User> volunteers = new ArrayList<User>();
    for(User u: my_users){
      if(u.getUserRole() == Role.VOLUNTEER){
        volunteers.add(u);
      }
    }
    return volunteers;
  }
  
  /**
   * @author James Gunter
   * Gets a list of all blackballed users.
   * @return A list of blackballed users.
   * 
   * pre: none, can be called at any time.
   * post: nothing has been modified.
   */
  public List<User> getBlackballed(){
    List<User> blackballed = new ArrayList<User>();
    for(User u: my_users){
      if(u.getUserRole() == Role.BLACKBALLED){
        blackballed.add(u);
      }
    }
    return blackballed;
  }
  /**
   * @author James Gunter
   * Initiates a shutdown. The controller will save the data to persistent storage.
   * 
   * pre: none, can be called at any time.
   * post: all data has been saved to the storage file.
   */
  public void shutdown(){
    my_persistant_storage.storeData(my_parks, my_users, my_jobs, my_options);
  }

  /**
   * @author James Gunter
   * Finds a park object by the name of the park.
   * @param the_park_name The name of the park.
   * @return Either the park object or null if not found.
   * 
   * pre: the_park_name is a non-null object of type String.
   * post: nothing has been modified.
   */
  public Park getPark(String the_park_name) {
    Park found = null;
    for(Park p : my_parks){
      if(p.getName().equals(the_park_name)){
        found = p;
        break;
      }
    }
    return found;
  }
  
  /**
   * @author James Gunter
   * Gets a unique List of park names.
   * @return A unique list of all park names.
   * 
   * pre: none, can be called at any time.
   * post: nothing has been modified.
   */
  public List<String> getParkNames() {
    Set<String> park_name_set = new HashSet<String>();
    for(Park p : my_parks){
      park_name_set.add(p.getName());
    }
    
    List<String> park_name_list = new ArrayList<String>();
    for(String s : park_name_set){
      park_name_list.add(s);
    }
    return park_name_list;
  }
}

